﻿using System.Net.Http.Headers;
using System.Security.Claims;
using Devonline.Core;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;

namespace Devonline.CloudService.Tencent.Weixin;

/// <summary>
/// 微信公众平台接入接口
/// </summary>
/// <param name="logger"></param>
/// <param name="endpoint">微信服务器接口配置</param>
/// <param name="httpContextAccessor"></param>
/// <param name="cache"></param>
/// <param name="httpClientFactory">http client 工厂, 需要在全局配置微信服务器接口地址</param>
public class WeixinService(
    ILogger<WeixinService> logger,
    IWeixinEndpoint endpoint,
    IHttpContextAccessor httpContextAccessor,
    IDistributedCache cache,
    IHttpClientFactory httpClientFactory
    ) : IWeixinService
{
    protected readonly ILogger<WeixinService> _logger = logger;
    protected readonly IDistributedCache _cache = cache;
    protected readonly IWeixinEndpoint _endpoint = endpoint;
    protected readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
    protected readonly HttpContext _httpContext = httpContextAccessor.HttpContext!;

    /// <summary>
    /// 微信登录
    /// docs:
    /// app: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html
    /// web: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
    /// </summary>
    /// <param name="code"></param>
    /// <param name="state"></param>
    /// <returns></returns>
    /// <exception cref="UnauthorizedAccessException"></exception>
    public virtual async Task AuthorizeAsync(string code, string? state = null)
    {
        _logger.LogInformation("已收到微信用户授权回调请求!");
        var accessToken = await GetAccessTokenAsync(code);
        var userInfo = await GetUserInfoAsync(accessToken.AccessToken, accessToken.OpenId) ?? throw new UnauthorizedAccessException("未能获取微信用户信息!");

        _logger.LogInformation($"将根据微信用户: {userInfo.OpenId} 构造微信认证信息!");
        var claims = new List<Claim>();
        claims.Add(new Claim(AppSettings.CLAIM_TYPE_JWT_SUBJECT, userInfo.UnionId!));
        claims.Add(new Claim(AppSettings.CLAIM_TYPE_OPEN_ID, userInfo.OpenId!));
        claims.Add(new Claim(nameof(WeixinUserInfo.Gender), userInfo.Gender.ToString()));
        claims.Add(new Claim(nameof(WeixinUserInfo.Gender), userInfo.Gender.ToString()));
        if (!string.IsNullOrWhiteSpace(userInfo.NickName))
        {
            claims.Add(new Claim(nameof(WeixinUserInfo.NickName), userInfo.NickName));
        }

        if (!string.IsNullOrWhiteSpace(userInfo.Country))
        {
            claims.Add(new Claim(nameof(WeixinUserInfo.Country), userInfo.Country));
        }

        if (!string.IsNullOrWhiteSpace(userInfo.Province))
        {
            claims.Add(new Claim(nameof(WeixinUserInfo.Province), userInfo.Province));
        }

        if (!string.IsNullOrWhiteSpace(userInfo.City))
        {
            claims.Add(new Claim(nameof(WeixinUserInfo.City), userInfo.City));
        }

        if (!string.IsNullOrWhiteSpace(userInfo.HeadImage))
        {
            claims.Add(new Claim(nameof(WeixinUserInfo.HeadImage), userInfo.HeadImage));
        }

        if (userInfo.Privilege is not null && userInfo.Privilege.Length > 0)
        {
            claims.Add(new Claim(nameof(WeixinUserInfo.Privilege), userInfo.Privilege.ToString(AppSettings.DEFAULT_OUTER_SPLITER)));
        }

        string scheme = CookieAuthenticationDefaults.AuthenticationScheme;
        var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, scheme));
        //await _httpContext.SignInAsync(scheme, claimsPrincipal, new AuthenticationProperties { IsPersistent = true, Items = { { nameof(scheme), nameof(Weixin) } } });
        await _httpContext.SignInAsync(scheme, claimsPrincipal, new AuthenticationProperties { IsPersistent = true, Items = { { nameof(scheme), scheme } } });
        _logger.LogInformation($"已根据微信用户: {userInfo.OpenId} 构造微信认证信息完成!");
        _httpContext.User = claimsPrincipal;
    }

    /// <summary>
    /// 获取 access token
    /// docs:
    /// 公众号: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
    /// 小程序: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
    /// </summary>
    /// <returns></returns>
    public virtual async Task<WeixinAccessToken> GetAccessTokenAsync(string? code = default)
    {
        _logger.LogInformation($"将根据微信用户授权码: {code} 获取访问令牌!");
        var url = string.IsNullOrWhiteSpace(code) ? $"/cgi-bin/token?appid={_endpoint.AppId}&secret={_endpoint.Secret}&grant_type=client_credential" : $"/sns/oauth2/access_token?appid={_endpoint.AppId}&secret={_endpoint.Secret}&grant_type=authorization_code&code={code}";
        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var response = await httpClient.GetStringAsync(url);

        try
        {
            GetWeixinResponse(response);
            var accessToken = response.ToJsonObject<WeixinAccessToken>();
            if (accessToken is not null && !string.IsNullOrWhiteSpace(accessToken.AccessToken) && accessToken.ExpiresIn > 0)
            {
                _logger.LogInformation($"微信用户 {accessToken.OpenId} 的访问令牌获取成功!");
                return accessToken;
            }

            throw new Exception("获取访问令牌失败, 服务器返回详情: " + response);
        }
        catch (Exception ex)
        {
            throw new Exception("获取访问令牌失败, 错误信息: " + ex.GetMessage(), ex);
        }
    }
    /// <summary>
    /// 获取稳定版 access token
    /// docs: 
    /// 公众号: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/getStableAccessToken.html
    /// 小程序: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html
    /// </summary>
    /// <param name="force">
    /// 是否强制获取, 默认使用 false。
    /// 1. force_refresh = false 时为普通调用模式，access_token 有效期内重复调用该接口不会更新 access_token；
    /// 2. 当force_refresh = true 时为强制刷新模式，会导致上次获取的 access_token 失效，并返回新的 access_token
    /// </param>
    /// <returns></returns>
    public virtual async Task<WeixinAccessToken> GetStableAccessTokenAsync(bool? force = false)
    {
        var cacheKey = AppSettings.CACHE_APPLICATION + nameof(WeixinAccessToken.AccessToken) + AppSettings.CHAR_UNDERLINE + _endpoint.AppId;
        var accessToken = await _cache.GetValueAsync<WeixinAccessToken>(cacheKey);
        if (accessToken is not null)
        {
            return accessToken;
        }

        _logger.LogInformation("未能从缓存中获取到 access token 或 access token 已过期, 将重新获取!");

        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var url = $"/cgi-bin/stable_token";
        using var httpContent = new StringContent(new WeixinStableToken { AppId = _endpoint.AppId!, Secret = _endpoint.Secret!, ForceRefresh = false }.ToJsonString(), new MediaTypeHeaderValue(ContentType.Txt));
        using var httpResponse = await httpClient.PostAsync(url, httpContent);
        var response = await httpResponse.Content.ReadAsStringAsync();
        if (!httpResponse.IsSuccessStatusCode)
        {
            throw new Exception($"获取稳定版访问令牌失败, 服务器返回状态: {httpResponse.StatusCode}, 服务器返回详情: " + response);
        }

        try
        {
            GetWeixinResponse(response);
            accessToken = response.ToJsonObject<WeixinAccessToken>();
            if (accessToken is not null && !string.IsNullOrWhiteSpace(accessToken.AccessToken) && accessToken.ExpiresIn > 0)
            {
                _logger.LogDebug("access token 获取成功!");
                await _cache.SetValueAsync(cacheKey, accessToken, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(accessToken.ExpiresIn) });
                return accessToken;
            }

            throw new Exception("获取稳定版访问令牌失败, 服务器返回详情: " + response);
        }
        catch (Exception ex)
        {
            throw new Exception("获取稳定版访问令牌失败, 错误信息: " + ex.GetMessage(), ex);
        }
    }
    /// <summary>
    /// 获取刷新 token
    /// </summary>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    public virtual Task<WeixinAccessToken> GetRefreshTokenAsync()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// 获取用户信息
    /// 微信公众平台需要先从客户端提交用户信息到服务器, 服务器端进行查找后, 在返回客户端
    /// 微信开放平台调用会直接从微信服务器端获取用户信息, 并进行查找后, 在返回客户端
    /// </summary>
    /// <param name="accessToken">access token</param>
    /// <param name="openId">微信用户的 openid</param>
    /// <returns></returns>
    public virtual async Task<WeixinUserInfo?> GetUserInfoAsync(string accessToken, string openId)
    {
        _logger.LogInformation($"将根据微信用户: {openId} 的访问令牌获取用户信息!");
        var url = $"/sns/userinfo?access_token={accessToken}&openid={openId}";
        using var httpClient = _httpClientFactory.CreateClient(nameof(WeixinService));
        var response = await httpClient.GetStringAsync(url) ?? throw new Exception("获取微信用户信息失败!");

        try
        {
            GetWeixinResponse(response);
            var userInfo = response.ToJsonObject<WeixinUserInfo>() ?? throw new Exception("微信用户信息获取失败, 服务器返回详情: " + response);
            _logger.LogInformation($"已根据微信用户: {openId} 的访问令牌获取到用户信息!");
            return userInfo;
        }
        catch (Exception ex)
        {
            throw new Exception("微信用户信息获取失败, 错误信息: " + ex.GetMessage(), ex);
        }
    }

    /// <summary>
    /// 从服务器响应获取 WeixinResponse, 无法转换时抛出异常
    /// </summary>
    /// <param name="response"></param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    protected WeixinResponse GetWeixinResponse(string response)
    {
        var weixinResponse = response.ToJsonObject<WeixinResponse>() ?? throw new Exception("获取微信返回值失败, 服务器返回详情: " + response); ;
        if (weixinResponse.ErrorCode != AppSettings.UNIT_ZERO)
        {
            throw new Exception($"获取微信返回值失败, 服务器反馈: error code {weixinResponse.ErrorCode}, error message: {weixinResponse.ErrorMessage}");
        }

        return weixinResponse;
    }
}